Skip to content

Comments

fix: fixed cumulativeGasUsed calculations in eth_getBlockReceipts and eth_getTransactionReceipt (#4921)#4936

Open
BartoszSolkaBD wants to merge 3 commits intohiero-ledger:mainfrom
BartoszSolkaBD:4921-Fix-cumulativeGasUsed--use-per-transaction-cumulative-gas,-not-block-total
Open

fix: fixed cumulativeGasUsed calculations in eth_getBlockReceipts and eth_getTransactionReceipt (#4921)#4936
BartoszSolkaBD wants to merge 3 commits intohiero-ledger:mainfrom
BartoszSolkaBD:4921-Fix-cumulativeGasUsed--use-per-transaction-cumulative-gas,-not-block-total

Conversation

@BartoszSolkaBD
Copy link
Contributor

@BartoszSolkaBD BartoszSolkaBD commented Feb 17, 2026

Description

This PR fixes an issue where cumulativeGasUsed in transaction receipts was incorrectly set to the block’s total gas used (block_gas_used) for every transaction in the block.

It updates the receipt generation logic so that cumulativeGasUsed reflects the cumulative gas consumed in the block up to and including each transaction, matching the Ethereum Yellow Paper semantics.

This change has been implemented for both eth_getBlockReceipts and eth_getTransactionReceipt.

It also updates the receipts root hash computation (and its expected test value) to use the corrected cumulative gas and adds targeted tests to validate multi‑transaction block behavior and OpenRPC contract for these endpoints.

Related issue(s)

Fixes #4921

Testing Guide

  1. Pick a block with multiple receipts, e.g. 0x56e7e72.
  2. Call eth_getBlockReceipts for that block.
  3. Observe that every receipt in the response has the correct cumulativeGasUsed value, which is increasing per transaction as defined in the Ethereum Yellow Paper.

Changes from original design (optional)

N/A

Additional work needed (optional)

N/A

Checklist

  • I've assigned an assignee to this PR and related issue(s) (if applicable)
  • I've assigned a label to this PR and related issue(s) (if applicable)
  • I've assigned a milestone to this PR and related issue(s) (if applicable)
  • I've updated documentation (code comments, README, etc. if applicable)
  • I've done sufficient testing (unit, integration, etc.)

… eth_getTransactionReceipt (hiero-ledger#4921)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
@BartoszSolkaBD BartoszSolkaBD requested review from a team as code owners February 17, 2026 12:59
@BartoszSolkaBD BartoszSolkaBD self-assigned this Feb 17, 2026
@BartoszSolkaBD BartoszSolkaBD added the bug Something isn't working label Feb 17, 2026
@BartoszSolkaBD BartoszSolkaBD added this to the 0.76.0 milestone Feb 17, 2026
Copy link
Contributor

@jasuwienas jasuwienas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great overall, but I have a few questions.

for (const item of items) {
const { transactionIndex, logsPerTx, crPerTx } = item;

const gasUsed = crPerTx.length && crPerTx[0].gas_used != null ? crPerTx[0].gas_used : 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const gasUsed = crPerTx.length && crPerTx[0].gas_used != null ? crPerTx[0].gas_used : 0;
const gasUsed = crPerTx[0]?.gas_used ?? 0;


const gasUsed = crPerTx.length && crPerTx[0].gas_used != null ? crPerTx[0].gas_used : 0;
cumulativeGas += gasUsed;
const transactionIndexHex = intToHex(transactionIndex);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still might have -1 here...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will use 0 as a fallback value. Do you see any risks in doing that?

const receiptPromises = contractResults.map(async (contractResult) => {
// Ensure contract results are processed in transaction_index (block) order
const sortedContractResults = [...contractResults].sort((a, b) => {
const aIdx = a.transaction_index ?? Number.MAX_SAFE_INTEGER;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should we sort by eventually? In previous method we used:

crPerTx[0].transaction_index

but when it was missing we checked:

logsPerTx[0].transactionIndex

now we are taking only cr, right?

Since we are fetching them all with a single request woulnd't it be posisble to fget them fetched from mirrorndoe right away, instead of sorting them on our own?

Aren;'t they, by any chance already sorted?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in this case, we should first create all receipts, both regular and synthetic, and then do another loop to calculate cumulativeGasUsed for each.

Synthetic receipts have 0 gasUsed, but it doesn't imply they should have 0 as cumulativeGasUsed. Please let me know what you think about this approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I get it now, synthetics dont ever influnce this value since they always use 0 gas.In this case I don't think we should take them into consideration here.

BUT we need to set this cumulativeGasPrice for the synthetic receipts generated here:
https://github.com/hiero-ledger/hiero-json-rpc-relay/pull/4936/changes/BASE..586dec6a6ba4dd355f07abae9d2425755e928503#diff-b8e1972336c5bbc108e68c01afa6bf2999c059bf3ea9ee67642d8a906f57a45eR444

as well. Which we aren't doing now, right?

Comment on lines +142 to +143
const logsPerTx: Log[] = logs.filter((log) => log.transactionHash === txHash);
const crPerTx: any[] = contractResults.filter((cr) => cr.hash === txHash);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Don't we really know what is the type of crPerTx?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should be able to type it. Maybe we can create another issue to fix it everywhere in the codebase?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, it’s not that important, it just crossed my mind while I was checking the PR. We can address it in a separate issue, we already have this problem in the code.

Comment on lines +498 to +502
const params: IContractResultsParams = {
blockNumber: receiptResponse.block_number,
};

const blockContractResults = await this.mirrorNodeClient.getContractResults(requestDetails, params);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I really don’t like the idea of fetching heavy block data for every transaction just to retrieve this single value :/ . But I don't know what to do about that, I really think this should be just calcualted in the mirrornode.

q:
Is there any correlation between the transaction index and the timestamp? Since this is the Hashgraph overall, consensus timestamps should matter for ordering, right? Maybe it applies to the on-block orders and we can fetch only the transactions with a timestamp lower than or equal to the one we’re querying for? (or would there be a problem with internal transactions for examlple?)

Also, we’re ignoring the fallback tx index from logs and only using the one from contract results. Wont that create differences for a single transaction when comparing it across these different endpoints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.hedera.com/api-reference/contracts/list-contract-results-from-all-contracts-on-the-network#parameter-transaction-index - there is an option to even query by transaction index x >= 0. A thing to discuss is, if we want to ask mirror node team to calculate this value on their side, or we do it ourselves and optimize what we can

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, I didn’t anticipate all of these implications when we were preparing the initial issue. What I do know is that fetching the block can be a very heavy operation (and I think it happens far less frequently than fetching a transaction receipt).
Including this could have significant performance implications with potentially limited benefit. I think we should all consider the options:

  1. Ignore this value for this particular endpoint - worst solution imo, since it would make the same transaction look different depending on where it’s queried.
  2. Accept the performance impact (investigate how significant it actually is) and proceed with the full implementation. We should also try to filter the results as much as possible. If filtering by transaction index works (assuming it’s the same transaction.index we’re referring to: I’m not entirely sure, as it sometimes appears to come from logs) or timestamps, we should do it deffinitely.
  3. (Keep the current “fake” value for cumulativeGasUsed - but clearly document that behavior and do this operation on the MN side.)

…iero-ledger#4921)

Signed-off-by: Bartosz Solka <bartosz.solka@blockydevs.com>
@codecov
Copy link

codecov bot commented Feb 20, 2026

Codecov Report

❌ Patch coverage is 71.59091% with 25 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ib/services/ethService/blockService/blockWorker.ts 68.62% 16 Missing ⚠️
...thService/transactionService/TransactionService.ts 73.52% 9 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (e0cea90) and HEAD (e999c06). Click for more details.

HEAD has 48 uploads less than BASE
Flag BASE (e0cea90) HEAD (e999c06)
config-service 2 0
relay 2 0
server 2 0
ws-server 2 0
42 2
@@             Coverage Diff             @@
##             main    #4936       +/-   ##
===========================================
- Coverage   95.94%   70.34%   -25.60%     
===========================================
  Files         143      143               
  Lines       23701    23839      +138     
  Branches     1877      632     -1245     
===========================================
- Hits        22740    16770     -5970     
- Misses        937     7052     +6115     
+ Partials       24       17        -7     
Flag Coverage Δ
config-service ?
relay ?
server ?
ws-server ?

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...lay/src/lib/factories/transactionReceiptFactory.ts 79.08% <100.00%> (-20.92%) ⬇️
...thService/transactionService/TransactionService.ts 63.29% <73.52%> (-36.33%) ⬇️
...ib/services/ethService/blockService/blockWorker.ts 67.24% <68.62%> (-31.60%) ⬇️

... and 85 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix cumulativeGasUsed: use per-transaction cumulative gas, not block total

2 participants